﻿using System;
using System.Collections.Concurrent;
using System.Threading;
using FlitBit.Core;
using FlitBit.Core.Parallel;
using FlitBit.Dto;
using FlitBit.Emit;
using FlitBit.ObjectIdentity;
using FlitBit.Represent;
using FlitBit.Represent.Bson;
using FlitBit.Wireup;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FlitBit.Cache.Tests
{
	[TestClass]
	public class RemoteRepresentationCacheTests
	{

		public TestContext TestContext { get; set; }

		[TestInitialize]
		public void Init()
		{
			RuntimeAssemblies.WriteDynamicAssemblyOnExit = true;
			WireupCoordinator.SelfConfigure();
		}

		[TestCleanup]
		public void Cleanup()
		{
			var report = WireupCoordinator.Instance.ReportWireupHistory();
			TestContext.WriteLine("---------- Wireup Report ----------");
			TestContext.WriteLine(report);
		}

		[TestMethod]
		public void RemoteRepresentationCache_CanPutAndGet()
		{
			var test = new { Iterations = 100 };
			// The FlitBit.Dto library provides the implementation of IMyCachedItem (by emitting it)
			// The FlitBit.Represent library provides the BSON representation (by emitting it)

			// Catch any unexpected parallel exceptions caused by our operations...
			var uncaughtException = default(Exception);
			Go.OnUncaughtException += (sender, e) => 
			{
				uncaughtException = e.Error;				
			};

			var random = new Random(Environment.TickCount);
			var gen = new DataGenerator();
			var factory = FactoryProvider.Factory;
			var represent = factory.CreateInstance<IBsonRepresentation<IMyCachedItem>>();
			Assert.IsNotNull(represent);

			var cache = new FakeRemoteRepresentationCache<int, IMyCachedItem, byte[]>("MyCachedItems", represent);

			var items = new ConcurrentDictionary<int, Tuple<IMyCachedItem, bool, Exception>>();

			for (int i = 0; i < test.Iterations; i++)
			{
				IMyCachedItem it;

				// get an id not already in the cache...
				var id = gen.GetInt32();
				while (cache.TryGet(id, out it))
				{
					id = gen.GetInt32();
				}

				it = factory.CreateInstance<IMyCachedItem>();
				it.ID = id;
				
				// fill the instance with random data...
				it.StringValue = gen.GetWords(random.Next(10, 100));
				it.IntValue = gen.GetInt32();
				it.DecimalValue = gen.GetDecimal();

				cache.Put(it.ID, it,
					(e, res) =>
					{
						Assert.IsNull(e);
						Assert.IsNotNull(res);

						Assert.AreEqual(it, res.Item2);

						items.TryAdd(it.ID, Tuple.Create(res.Item2, true, e));
					});			 
			}

			while (items.Count < test.Iterations)
			{
				Thread.Sleep(0);
			}

			// Assert there was no unexpected parallel exception...
			Assert.IsNull(uncaughtException);
		}
	}

	[DTO]
	public interface IMyCachedItem
	{
		[IdentityKey]
		int ID { get; set; }

		string StringValue { get; set; }

		int IntValue { get; set; }

		decimal DecimalValue { get; set; }
	}

	class FakeRemoteRepresentationCache<TKey, TValue, TRep> : RemoteRepresentationCache<TKey, TValue, TRep>
	{
		ConcurrentDictionary<TKey, TRep> _store = new ConcurrentDictionary<TKey, TRep>();
		Object _sync = new Object();
		Random _rand = new Random();
		int SimulatedLatency
		{
			get
			{
				lock (_sync)
				{
					return _rand.Next(200);
				}
			}
		}

		public FakeRemoteRepresentationCache(string name, IRepresentation<TValue, TRep> represent)
			: base(name, represent)
		{
		}

		protected override void RemoteGet(TKey key, Continuation<Tuple<TKey, TRep>> continuation)
		{
			Go.Parallel(() =>
			{
				Thread.Sleep(SimulatedLatency);

				try
				{
					TRep value;
					var hasValue = _store.TryGetValue(key, out value);
					try
					{
						if (hasValue)
						{
							continuation(null, Tuple.Create(key, value));
						}
						else
						{
							continuation(null, null);
						}
					}
					catch (Exception e)
					{
						Go.NotifyUncaughtException(continuation.Target, e);
					}
				}
				catch (Exception e)
				{
					continuation(e, null);
				}
			});
		}

		protected override void RemotePut(TKey key, TRep item, Continuation continuation)
		{
			Go.Parallel(() =>
			{
				Thread.Sleep(SimulatedLatency);

				try
				{
					this._store.AddOrUpdate(key, item, (k, r) => item);
					try
					{
						continuation(null);
					}
					catch (Exception e)
					{
						Go.NotifyUncaughtException(continuation.Target, e);
					}
				}
				catch (Exception e)
				{
					continuation(e);
				}
			});
		}

		protected override void RemoteRemove(TKey key, Continuation<Tuple<TKey, TRep>> continuation)
		{
			Go.Parallel(() =>
			{
				Thread.Sleep(SimulatedLatency);

				try
				{
					TRep value;
					var hasValue = _store.TryRemove(key, out value);
					try
					{
						if (hasValue)
						{
							continuation(null, Tuple.Create(key, value));
						}
						else
						{
							continuation(null, null);
						}
					}
					catch (Exception e)
					{
						Go.NotifyUncaughtException(continuation.Target, e);
					}
				}
				catch (Exception e)
				{
					continuation(e, null);
				}
			});
		}

		protected override bool PerformDispose(bool disposing)
		{
			return disposing;
		}
	}
}
